/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactMultiChild
 */

'use strict';

var ReactComponentEnvironment = require('ReactComponentEnvironment');
var ReactInstanceMap = require('ReactInstanceMap');
var ReactInstrumentation = require('ReactInstrumentation');

var ReactCurrentOwner = require('react/lib/ReactCurrentOwner');
var ReactReconciler = require('ReactReconciler');
var ReactChildReconciler = require('ReactChildReconciler');

var emptyFunction = require('fbjs/lib/emptyFunction');
var flattenChildren = require('flattenChildren');
var invariant = require('fbjs/lib/invariant');

/**
 * Make an update for markup to be rendered and inserted at a supplied index.
 *
 * @param {string} markup Markup that renders into an element.
 * @param {number} toIndex Destination index.
 * @private
 */
function makeInsertMarkup(markup, afterNode, toIndex) {
  // NOTE: Null values reduce hidden classes.
  return {
    type: 'INSERT_MARKUP',
    content: markup,
    fromIndex: null,
    fromNode: null,
    toIndex: toIndex,
    afterNode: afterNode,
  };
}

/**
 * Make an update for moving an existing element to another index.
 *
 * @param {number} fromIndex Source index of the existing element.
 * @param {number} toIndex Destination index of the element.
 * @private
 */
function makeMove(child, afterNode, toIndex) {
  // NOTE: Null values reduce hidden classes.
  return {
    type: 'MOVE_EXISTING',
    content: null,
    fromIndex: child._mountIndex,
    fromNode: ReactReconciler.getHostNode(child),
    toIndex: toIndex,
    afterNode: afterNode,
  };
}

/**
 * Make an update for removing an element at an index.
 *
 * @param {number} fromIndex Index of the element to remove.
 * @private
 */
function makeRemove(child, node) {
  // NOTE: Null values reduce hidden classes.
  return {
    type: 'REMOVE_NODE',
    content: null,
    fromIndex: child._mountIndex,
    fromNode: node,
    toIndex: null,
    afterNode: null,
  };
}

/**
 * Make an update for setting the markup of a node.
 *
 * @param {string} markup Markup that renders into an element.
 * @private
 */
function makeSetMarkup(markup) {
  // NOTE: Null values reduce hidden classes.
  return {
    type: 'SET_MARKUP',
    content: markup,
    fromIndex: null,
    fromNode: null,
    toIndex: null,
    afterNode: null,
  };
}

/**
 * Make an update for setting the text content.
 *
 * @param {string} textContent Text content to set.
 * @private
 */
function makeTextContent(textContent) {
  // NOTE: Null values reduce hidden classes.
  return {
    type: 'TEXT_CONTENT',
    content: textContent,
    fromIndex: null,
    fromNode: null,
    toIndex: null,
    afterNode: null,
  };
}

/**
 * Push an update, if any, onto the queue. Creates a new queue if none is
 * passed and always returns the queue. Mutative.
 */
function enqueue(queue, update) {
  if (update) {
    queue = queue || [];
    queue.push(update);
  }
  return queue;
}

/**
 * Processes any enqueued updates.
 *
 * @private
 */
function processQueue(inst, updateQueue) {
  ReactComponentEnvironment.processChildrenUpdates(
    inst,
    updateQueue,
  );
}

var setChildrenForInstrumentation = emptyFunction;
if (__DEV__) {
  var getDebugID = function(inst) {
    if (!inst._debugID) {
      // Check for ART-like instances. TODO: This is silly/gross.
      var internal;
      if ((internal = ReactInstanceMap.get(inst))) {
        inst = internal;
      }
    }
    return inst._debugID;
  };
  setChildrenForInstrumentation = function(children) {
    var debugID = getDebugID(this);
    // TODO: React Native empty components are also multichild.
    // This means they still get into this method but don't have _debugID.
    if (debugID !== 0) {
      ReactInstrumentation.debugTool.onSetChildren(
        debugID,
        children ? Object.keys(children).map(key => children[key]._debugID) : []
      );
    }
  };
}

/**
 * Provides common functionality for components that must reconcile multiple
 * children. This is used by `ReactDOMComponent` to mount, update, and
 * unmount child components.
 */
var ReactMultiChild = {
  _reconcilerInstantiateChildren: function(nestedChildren, transaction, context) {
    if (__DEV__) {
      var selfDebugID = getDebugID(this);
      if (this._currentElement) {
        try {
          ReactCurrentOwner.current = this._currentElement._owner;
          return ReactChildReconciler.instantiateChildren(
            nestedChildren, transaction, context, selfDebugID
          );
        } finally {
          ReactCurrentOwner.current = null;
        }
      }
    }
    return ReactChildReconciler.instantiateChildren(
      nestedChildren, transaction, context
    );
  },

  _reconcilerUpdateChildren: function(
    prevChildren,
    nextNestedChildrenElements,
    mountImages,
    removedNodes,
    transaction,
    context
  ) {
    var nextChildren;
    var selfDebugID = 0;
    if (__DEV__) {
      selfDebugID = getDebugID(this);
      if (this._currentElement) {
        try {
          ReactCurrentOwner.current = this._currentElement._owner;
          nextChildren = flattenChildren(nextNestedChildrenElements, selfDebugID);
        } finally {
          ReactCurrentOwner.current = null;
        }
        ReactChildReconciler.updateChildren(
          prevChildren,
          nextChildren,
          mountImages,
          removedNodes,
          transaction,
          this,
          this._hostContainerInfo,
          context,
          selfDebugID
        );
        return nextChildren;
      }
    }
    nextChildren = flattenChildren(nextNestedChildrenElements, selfDebugID);
    ReactChildReconciler.updateChildren(
      prevChildren,
      nextChildren,
      mountImages,
      removedNodes,
      transaction,
      this,
      this._hostContainerInfo,
      context,
      selfDebugID
    );
    return nextChildren;
  },

  /**
   * Generates a "mount image" for each of the supplied children. In the case
   * of `ReactDOMComponent`, a mount image is a string of markup.
   *
   * @param {?object} nestedChildren Nested child maps.
   * @return {array} An array of mounted representations.
   * @internal
   */
  mountChildren: function(nestedChildren, transaction, context) {
    var children = this._reconcilerInstantiateChildren(
      nestedChildren, transaction, context
    );
    this._renderedChildren = children;

    var mountImages = [];
    var index = 0;
    for (var name in children) {
      if (children.hasOwnProperty(name)) {
        var child = children[name];
        var selfDebugID = 0;
        if (__DEV__) {
          selfDebugID = getDebugID(this);
        }
        var mountImage = ReactReconciler.mountComponent(
          child,
          transaction,
          this,
          this._hostContainerInfo,
          context,
          selfDebugID
        );
        child._mountIndex = index++;
        mountImages.push(mountImage);
      }
    }

    if (__DEV__) {
      setChildrenForInstrumentation.call(this, children);
    }

    return mountImages;
  },

  /**
   * Replaces any rendered children with a text content string.
   *
   * @param {string} nextContent String of content.
   * @internal
   */
  updateTextContent: function(nextContent) {
    var prevChildren = this._renderedChildren;
    // Remove any rendered children.
    ReactChildReconciler.unmountChildren(prevChildren, false);
    for (var name in prevChildren) {
      if (prevChildren.hasOwnProperty(name)) {
        invariant(false, 'updateTextContent called on non-empty component.');
      }
    }
    // Set new text content.
    var updates = [makeTextContent(nextContent)];
    processQueue(this, updates);
  },

  /**
   * Replaces any rendered children with a markup string.
   *
   * @param {string} nextMarkup String of markup.
   * @internal
   */
  updateMarkup: function(nextMarkup) {
    var prevChildren = this._renderedChildren;
    // Remove any rendered children.
    ReactChildReconciler.unmountChildren(prevChildren, false);
    for (var name in prevChildren) {
      if (prevChildren.hasOwnProperty(name)) {
        invariant(false, 'updateTextContent called on non-empty component.');
      }
    }
    var updates = [makeSetMarkup(nextMarkup)];
    processQueue(this, updates);
  },

  /**
   * Updates the rendered children with new children.
   *
   * @param {?object} nextNestedChildrenElements Nested child element maps.
   * @param {ReactReconcileTransaction} transaction
   * @internal
   */
  updateChildren: function(nextNestedChildrenElements, transaction, context) {
    // Hook used by React ART
    this._updateChildren(nextNestedChildrenElements, transaction, context);
  },

  /**
   * @param {?object} nextNestedChildrenElements Nested child element maps.
   * @param {ReactReconcileTransaction} transaction
   * @final
   * @protected
   */
  _updateChildren: function(nextNestedChildrenElements, transaction, context) {
    var prevChildren = this._renderedChildren;
    var removedNodes = {};
    var mountImages = [];
    var nextChildren = this._reconcilerUpdateChildren(
      prevChildren,
      nextNestedChildrenElements,
      mountImages,
      removedNodes,
      transaction,
      context
    );
    if (!nextChildren && !prevChildren) {
      return;
    }
    var updates = null;
    var name;
    // `nextIndex` will increment for each child in `nextChildren`, but
    // `lastIndex` will be the last index visited in `prevChildren`.
    var nextIndex = 0;
    var lastIndex = 0;
    // `nextMountIndex` will increment for each newly mounted child.
    var nextMountIndex = 0;
    var lastPlacedNode = null;
    for (name in nextChildren) {
      if (!nextChildren.hasOwnProperty(name)) {
        continue;
      }
      var prevChild = prevChildren && prevChildren[name];
      var nextChild = nextChildren[name];
      if (prevChild === nextChild) {
        updates = enqueue(
          updates,
          this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex)
        );
        lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        prevChild._mountIndex = nextIndex;
      } else {
        if (prevChild) {
          // Update `lastIndex` before `_mountIndex` gets unset by unmounting.
          lastIndex = Math.max(prevChild._mountIndex, lastIndex);
          // The `removedNodes` loop below will actually remove the child.
        }
        // The child must be instantiated before it's mounted.
        updates = enqueue(
          updates,
          this._mountChildAtIndex(
            nextChild,
            mountImages[nextMountIndex],
            lastPlacedNode,
            nextIndex,
            transaction,
            context
          )
        );
        nextMountIndex++;
      }
      nextIndex++;
      lastPlacedNode = ReactReconciler.getHostNode(nextChild);
    }
    // Remove children that are no longer present.
    for (name in removedNodes) {
      if (removedNodes.hasOwnProperty(name)) {
        updates = enqueue(
          updates,
          this._unmountChild(prevChildren[name], removedNodes[name])
        );
      }
    }
    if (updates) {
      processQueue(this, updates);
    }
    this._renderedChildren = nextChildren;

    if (__DEV__) {
      setChildrenForInstrumentation.call(this, nextChildren);
    }
  },

  /**
   * Unmounts all rendered children. This should be used to clean up children
   * when this component is unmounted. It does not actually perform any
   * backend operations.
   *
   * @internal
   */
  unmountChildren: function(safely) {
    var renderedChildren = this._renderedChildren;
    ReactChildReconciler.unmountChildren(renderedChildren, safely);
    this._renderedChildren = null;
  },

  /**
   * Moves a child component to the supplied index.
   *
   * @param {ReactComponent} child Component to move.
   * @param {number} toIndex Destination index of the element.
   * @param {number} lastIndex Last index visited of the siblings of `child`.
   * @protected
   */
  moveChild: function(child, afterNode, toIndex, lastIndex) {
    // If the index of `child` is less than `lastIndex`, then it needs to
    // be moved. Otherwise, we do not need to move it because a child will be
    // inserted or moved before `child`.
    if (child._mountIndex < lastIndex) {
      return makeMove(child, afterNode, toIndex);
    }
  },

  /**
   * Creates a child component.
   *
   * @param {ReactComponent} child Component to create.
   * @param {string} mountImage Markup to insert.
   * @protected
   */
  createChild: function(child, afterNode, mountImage) {
    return makeInsertMarkup(mountImage, afterNode, child._mountIndex);
  },

  /**
   * Removes a child component.
   *
   * @param {ReactComponent} child Child to remove.
   * @protected
   */
  removeChild: function(child, node) {
    return makeRemove(child, node);
  },

  /**
   * Mounts a child with the supplied name.
   *
   * NOTE: This is part of `updateChildren` and is here for readability.
   *
   * @param {ReactComponent} child Component to mount.
   * @param {string} name Name of the child.
   * @param {number} index Index at which to insert the child.
   * @param {ReactReconcileTransaction} transaction
   * @private
   */
  _mountChildAtIndex: function(
    child,
    mountImage,
    afterNode,
    index,
    transaction,
    context) {
    child._mountIndex = index;
    return this.createChild(child, afterNode, mountImage);
  },

  /**
   * Unmounts a rendered child.
   *
   * NOTE: This is part of `updateChildren` and is here for readability.
   *
   * @param {ReactComponent} child Component to unmount.
   * @private
   */
  _unmountChild: function(child, node) {
    var update = this.removeChild(child, node);
    child._mountIndex = null;
    return update;
  },
};

module.exports = ReactMultiChild;
